/**
 * Copyright (c) 2008, Song Hui, Peking University, All rights
 * reserved.
 */
package cn.edu.pku.sei.ra.framework;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.EModelElement;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.util.EcoreUtil;

import cn.edu.pku.sei.ra.util.ECoreRelatedUtil;

/**
 * 
 */
public class ModelDiffer {
	
	LinkedList<EReference> referenceStack=new LinkedList<EReference>();
	Resource result=null;
	
	ResourceEnv resEnv=null;
	
	Set added=new HashSet<EObject>();
	Set removed=new HashSet<EObject>();
	private Comparator<EStructuralFeature> comparator = new Comparator<EStructuralFeature>(){
			public int compare(EStructuralFeature o1,
		           EStructuralFeature o2){
				return o1.getName().compareTo(o2.getName());					
			}
		};
	/**
	 * 
	 */
	public ModelDiffer(ResourceEnv resEnv) {
		this.resEnv=resEnv;
	}
	/**
	 * calculate about "if we want to change runMod to just the 
	 * same as refMod, what modification we need".
	 * return the extra parts;
	 * @param refMod
	 * @param runMod
	 */
	public Resource diffResource(Resource refMod, Resource runMod){
		result=resEnv.createInterResource();
		result.getContents().addAll(EcoreUtil.copyAll(refMod.getContents()));
		diffElem((EObject)result.getContents().iterator().next(),(EObject)runMod.getContents().iterator().next());
		return result;
	}
	
	public boolean diffSet(EList refSet, EList runSet, 
							EStructuralFeature feature){
		boolean nochange=true;
		
		EClass clazz=(EClass)feature.getEType();
		
		List<EStructuralFeature> keys=ECoreRelatedUtil.getAllKeyFeatures(clazz);
				
		ArrayList<EObject> noNeeded=new ArrayList<EObject>();
		for(int i=0;i<refSet.size();i++){
			
			EObject ref=(EObject)refSet.get(i);
			EObject run=null;
			for(Object ob2:runSet){
				if(ECoreRelatedUtil.checkTheSame(ref,(EObject)ob2,keys)){
					run=(EObject)ob2;
					break;
				}
			}			
			if((feature instanceof EReference) && ((EReference)feature).isContainment()){
				if(run==null){
					added.add(ref);
					nochange=false;
				}
				else if(diffElem(ref,run)){
					noNeeded.add(ref);
				}
				else
					nochange=false;
			}
			else{
				if(run!=null)
					noNeeded.add(ref);
			}			
		}
		for(EObject eob:noNeeded){
			refSet.remove(eob);
		}		
		
		return nochange;
	}
	
	public boolean diffElem(EObject refElem, EObject runElem){
		boolean nochange=true;
		EClass clazz=refElem.eClass();
		
		EList<EAttribute> attrs=clazz.getEAllAttributes();
		
		
		for(EAttribute attr:attrs){
			if(ECoreRelatedUtil.isPrimaryAttribute(attr))
				continue;
			Object refValue=refElem.eGet(attr);
			Object runValue=runElem.eGet(attr);
			if(refValue==null&&runValue==null)
				continue;
			if(refValue!=null && refValue.equals(runValue)){
				if(!attr.getEType().getName().equals("EBoolean"))
					refElem.eSet(attr, null);
				continue;
			}
			else
				nochange=false;
		}
		
		EList<EReference> references=clazz.getEAllReferences();
		for(EReference reference:references){
			EReference op=reference.getEOpposite();
			if(op!=null && referenceStack.contains(op)){
				continue;
			}
			referenceStack.addLast(reference);
			
			if(reference.getUpperBound()==1){
				Object ref=refElem.eGet(reference);
				Object run=runElem.eGet(reference);
				if(ref==null && run==null)
					;
				else if(ref==null && run!=null){
					;
				}
				else if(ref!=null && run==null){
					if(reference.isContainment())
						added.add(ref);
					nochange=false;					
				}
				else{
					if(reference.isContainment()){
						if(diffElem((EObject)ref,(EObject)run))
							refElem.eSet(reference,null);
						else
							nochange=false;
					}
					else{
						if(checkTheSame((EObject)ref, (EObject)run)){
							refElem.eSet(reference,null);
						}
						else{
							nochange=false;
						}
					}
				}
			}
			else{
				if(diffSet((EList)refElem.eGet(reference),(EList)runElem.eGet(reference),reference)){
					;
				}
				else{
					nochange=false;
				}
			}
			referenceStack.removeLast();
		}
		
		return nochange;
	}
	public boolean checkTheSame(EObject o1, EObject o2)
	{
		List<EStructuralFeature> keys=ECoreRelatedUtil
			.getAllKeyFeatures(o1.eClass());
		return ECoreRelatedUtil.checkTheSame(o1,o2,keys);
	}

	
	EPackage pack=null;
	
	public void add(Resource desResource, EPackage pack){
		this.pack=pack;
		addElement(result.getContents().get(0),desResource.getContents().get(0));
	}
	
	public void remove(Resource desResource, EPackage pack){
		this.pack=pack;
		removeElement(result.getContents().get(0),desResource.getContents().get(0));
	}
	
	public void addElement(EObject refElem, EObject desElem){
		EClass refClass=refElem.eClass();	
		
		ArrayList<EAttribute> refAttrs=new ArrayList<EAttribute>();
		refAttrs.addAll(refClass.getEAllAttributes());
		ArrayList<EAttribute> desAttrs=new ArrayList<EAttribute>();
		EClass desClass=desElem.eClass();
		desAttrs.addAll(desClass.getEAllAttributes());
		
		Collections.sort(refAttrs,comparator);
		Collections.sort(desAttrs,comparator);
		
		for(int i=0, j=0;i<refAttrs.size();i++){
			while(comparator.compare(refAttrs.get(i),desAttrs.get(j))>0)
				j++;
			Object refValue=refElem.eGet(refAttrs.get(i));
			if(refValue!=null)
				desElem.eSet(desAttrs.get(j), refValue);
		}
		ArrayList<EReference> refRefers=new ArrayList<EReference>();
		refRefers.addAll(refClass.getEAllReferences());
		ArrayList<EReference> desRefers=new ArrayList<EReference>();
		desRefers.addAll(desClass.getEAllReferences());
		
		Collections.sort(refRefers,comparator);
		Collections.sort(desRefers,comparator);
		for(int i=0,j=0;i<refRefers.size();i++){
			while(comparator.compare(refRefers.get(i),desRefers.get(j))>0)
				j++;
			EReference refRefer=refRefers.get(i);
			EReference desRefer=desRefers.get(j);
			
			EReference op=refRefer.getEOpposite();
			if(op!=null && referenceStack.contains(op)){
				continue;
			}
			referenceStack.addLast(refRefer);
			if(refRefer.getUpperBound()==1){
				//TODO: How about single references?
			}
			else{				
				EList<EObject> refs=(EList<EObject>)refElem.eGet(refRefer);
				if(refRefer.isContainment()){				
					for(EObject ref:refs){
						EObject des=null;
						if(added.contains(ref)){						
							EClass clazz=(EClass)desRefer.getEType();
							if(clazz.isAbstract()){
								clazz=(EClass)clazz.getEPackage().getEClassifier(ref.eClass().getName());
							}
							des=pack.getEFactoryInstance().create(clazz);
							((EList)desElem.eGet(desRefer)).add(des);
						}
						else{
							EList<EObject> deses=(EList<EObject>)desElem.eGet(desRefer);
							for(EObject eobj:deses){
								if(ECoreRelatedUtil.checkTheSameFromDifferentPackage(ref,eobj)){
									des=eobj;
									break;
								}								
							}
						}
						addElement(ref,des);
					}
				
				}
				else{
					for(EObject ref:refs){
						// TODO: How to deal with references efficiently?
					}
				}
			}
			referenceStack.removeLast();
		}
	}
	
	public void removeElement(EObject refElem, EObject desElem){
		EClass refClass=refElem.eClass();		
		
		EClass desClass=desElem.eClass();
		
		ArrayList<EReference> refRefers=new ArrayList<EReference>();
		refRefers.addAll(refClass.getEAllReferences());
		ArrayList<EReference> desRefers=new ArrayList<EReference>();
		desRefers.addAll(desClass.getEAllReferences());
		Collections.sort(refRefers,comparator);
		Collections.sort(desRefers,comparator);
		
		for(int i=0,j=0;i<refRefers.size();i++){
			while(comparator.compare(refRefers.get(i),desRefers.get(j))>0)
				j++;
			EReference refRefer=refRefers.get(i);
			EReference desRefer=desRefers.get(j);
			
			EReference op=refRefer.getEOpposite();
			if(op!=null && referenceStack.contains(op)){
				continue;
			}
			referenceStack.addLast(refRefer);
			
			EList<EObject> refs=(EList<EObject>)refElem.eGet(refRefer);
			if(refs!=null)
				for(EObject ref:refs){
					EObject des=null;
					EList<EObject> deses=(EList<EObject>)desElem.eGet(desRefer);
					for(EObject eobj:deses){
						if(ECoreRelatedUtil.checkTheSameFromDifferentPackage(ref,eobj)){
							des=eobj;
							break;
						}								
					}
					if(refRefer.isContainment() && !added.contains(ref)){
						removeElement(ref,des);
					}
					else
						((EList)desElem.eGet(desRefer)).remove(des);
				}
			
			referenceStack.removeLast();
		}
	}
}
